classdef dotsTheDrawablesManager < dotsAllRemoteObjectManagers
    % @class dotsTheDrawablesManager
    % Coordinate dotsDrawable* objects and dotsTheScreen.
    
    properties
        isAnimating;
        isAccumulating;
        
        activeGroup;
        activeDrawables;
        droppedFrameList;
        
        theScreen;
        lastFrameTime;
        
        % Psychtoolbox Scxreen index of the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowNumber;
        
        % pixel dimensions of the current drawing window [x y x2 y2]
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowRect;
        
        % Hz frame rate of the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        windowFrameRate;
        
        % degrees-to-pixels factor for the current drawing window
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        pixelsPerDegree;
        
        textSettings;
        
        afterFrameFevalable;
        nextFrameFunction;
        
        % a foreground color, [L] [LA] [RGB] [RGBA], 0-255
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        foregroundColor;
        
        % a background color, [L] [LA] [RGB] [RGBA], 0-255
        % @details
        % Automatically opts in to share via dotsTheSwitchboard.
        backgroundColor;
    end
    
    methods (Access=private)
        function self = dotsTheDrawablesManager
            self.initialize;
        end
    end
    
    methods (Static=true)
        function dm = theObject
            persistent self
            if isempty(self) || ~isvalid(self)
                self = dotsTheDrawablesManager;
            end
            dm = self;
        end
        
        function reset
            self = dotsTheDrawablesManager.theObject;
            self.initialize;
        end
        
        function g = gui
            self = dotsTheDrawablesManager.theObject;
            g = self.managedObjects.gui;
        end
    end
    
    methods
        % Restore the manager singleton to a fresh state.
        function initialize(self)
            self.initialize@dotsAllRemoteObjectManagers;
            self.initializeLists({'droppedFrameList'});
            
            self.shouldForwardToCounterpart = false;
            self.shouldCallToCounterpart = false;
            
            % get shared property values from the screen
            self.theScreen = dotsTheScreen.theObject;
            dotsTheSwitchboard.shareManyPropertiesOfObject( ...
                self.theScreen.sharedProperties, self);
            
            self.isAnimating = false;
            self.isAccumulating = false;
            self.lastFrameTime = [];
            self.activeGroup = 'none';
            
            % serverMode behavior should dominate
            if self.clientMode && ~self.serverMode
                self.nextFrameFunction = @requestNextFrame;
            else
                self.nextFrameFunction = @drawNextFrame;
            end
            
            self.shouldForwardToCounterpart = true;
            self.shouldCallToCounterpart = true;
        end
        
        function addDrawableToGroupWithRank(self, drawable, group, rank)
            self.addToList(drawable, group, rank);
            txn = self.transactionTemplate;
            txn.type = 'join';
            txn.objectGroup = group;
            txn.objectUID = drawable.UID;
            txn.methodArgs = rank;
            self.openTransaction(txn);
        end
        
        function set.activeGroup(self, activeGroup)
            self.activeGroup = activeGroup;
            if any(strcmp(self.managedObjects.groups, activeGroup))
                self.activeDrawables = self.getGroup(activeGroup);
                self.prepareActiveGroupForWindow;
            else
                self.activeDrawables = {};
            end
            self.forwardToCounterpart('activeGroup', activeGroup);
        end
        
        function set.isAccumulating(self, isAccumulating)
            self.isAccumulating = isAccumulating;
            self.forwardToCounterpart('isAccumulating', isAccumulating);
        end
        
        function set.lastFrameTime(self, lastFrameTime)
            self.lastFrameTime = lastFrameTime;
            if ~isempty(lastFrameTime)
                if ~isempty(self.afterFrameFevalable)
                    feval(self.afterFrameFevalable{:});
                end
            end
        end
        
        function openScreenWindow(self)
            if self.clientMode && ~self.serverMode
                % request a window
                self.finishAllTransactions;
                self.callToCounterpart('openScreenWindow');
                self.finishAllTransactions;
                self.requestScreenSharedProperties;
                self.finishAllTransactions;

            else
                % open a real window
                self.theScreen.open;
                self.prepareActiveGroupForWindow;
            end
        end
        
        function closeScreenWindow(self)
            if self.clientMode && ~self.serverMode
                % request the window closed
                self.finishAllTransactions;
                self.callToCounterpart('closeScreenWindow');
                self.finishAllTransactions;
                self.requestScreenSharedProperties;
                self.finishAllTransactions;

            else
                % close the window
                self.theScreen.close;
            end
        end
        
        function textSettings = setScreenTextSetting(self, setting, value)
            % set the all the settings for the local screen
            self.theScreen.textSettings.(setting) = value;

            % return any updates from the local screen
            textSettings = self.theScreen.textSettings;
            self.textSettings = textSettings;
            
            % forward the settings to the counterpart manager
            %   get back the counterpart's updated settings
            self.callToCounterpartWithReturn('textSettings', ...
                'setScreenTextSetting', setting, value);
        end
        
        function when = startAnimating(self)
            self.isAnimating = true;
            self.callToCounterpartWithReturn('lastFrameTime', 'startAnimating');
            when = self.mayDrawNextFrame;
            self.lastFrameTime = when;
        end
        
        function stopAnimating(self)
            self.isAnimating = false;
            self.callToCounterpart('stopAnimating');
        end
        
        function step(self)
            if self.serverMode
                self.receiveTransactionsAsServer(true);
            end
            
            if self.isAnimating && (self.serverMode || ~self.clientMode)
                self.mayDrawNextFrame;
            end
            
            if self.clientMode
                self.receiveTransactionsAsClient(true);
            end
        end
        
        function prepareActiveGroupForWindow(self)
            if self.windowNumber > 0
                drawList = self.activeDrawables;
                for ii = 1:length(drawList)
                    drawList{ii}.prepareToDrawInWindow;
                end
            end
        end
        
        function when = mayDrawNextFrame(self)
            when = feval(self.nextFrameFunction, self);
            self.lastFrameTime = when;
            % disp(sprintf('pending: %d, closed: %d', ...
            %     self.transactionsPending, self.transactionsClosed));
        end
        
        function when = drawNextFrame(self)
            begin = feval(self.clockFunction);
            
            drawList = self.activeDrawables;
            for ii = 1:length(drawList)
                drawList{ii}.mayDrawNow;
            end
            
            when = self.theScreen.nextFrame(self.isAccumulating);
            if ~isempty(when) && (when-begin) > self.theScreen.frameSize
                self.droppedFrameList.addItemToGroupWithMnemonic( ...
                    when-begin, self.activeGroup, begin);
            end
        end
        
        function when = requestNextFrame(self)
            when = [];
            self.finishAllTransactions;
            self.callToCounterpartWithReturn('lastFrameTime', 'drawNextFrame');
        end
        
        function value = getScreenProperty(self, property)
            value = self.theScreen.(property);
        end
    end
    
    methods (Access = protected)
        function acknowledgeTransaction(self, txn)
            self.acknowledgeTransaction@dotsAllRemoteObjectManagers(txn);
            
            if strcmp(txn.objectMethod, 'getScreenProperty')
                self.overwriteScreenSharedPropertyValue(txn.objectProperty);
            end
        end
        
        function requestScreenSharedProperties(self)
            % clear and request certain screen properties
            shared = self.theScreen.sharedProperties;
            for ii = 1:length(shared)
                self.setPropertySilently(shared{ii}, []);
                self.callToCounterpartWithReturn(shared{ii}, 'getScreenProperty', shared{ii});
            end
        end
        
        function overwriteScreenSharedPropertyValue(self, property)
            dotsTheSwitchboard.setSharedPropertyValue(property, self.(property));
        end
    end
end